package com.erbantou.filemanage.service.impl;

import com.erbantou.filemanage.service.CustomMinioClient;
import com.erbantou.filemanage.service.FileHashService;
import com.erbantou.filemanage.service.StorageService;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import io.minio.*;
import io.minio.errors.ErrorResponseException;
import io.minio.errors.MinioException;
import io.minio.messages.Item;
import io.minio.messages.Part;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * MinioStorageService 提供了与 MinIO 对象存储服务交互的方法
 */
@Service
@Slf4j
@ConditionalOnProperty(name = "custom.storage.service", havingValue = "MinioStorageService")
public class MinioStorageService implements StorageService {

    @Autowired
    private FileHashService fileHashService;

    // MinIO 配置参数
    @Value("${minio.url}")
    private String minioUrl;
    @Value("${minio.access-key}")
    private String minioAccessKey;
    @Value("${minio.secret-key}")
    private String minioSecretKey;
    @Value("${minio.bucket-name}")
    private String bucketName;

//    private final CustomMinioClient minioClient;
//
//    public MinioStorageService() {
//        this.minioClient = initializeMinioClient();
//    }

    /**
     * 初始化 MinIO 客户端并检查存储桶是否存在
     *
     * @return CustomMinioClient
     */
    private CustomMinioClient initializeMinioClient() {
        MinioClient minioClient = MinioClient.builder()
                .endpoint(minioUrl)
                .credentials(minioAccessKey, minioSecretKey)
                .build();
        CustomMinioClient customMinioClient = new CustomMinioClient(minioClient);
        try {
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!found) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            log.error("检查存储桶时发生错误：{}", e.toString());
        }
        return customMinioClient;
    }

    /**
     * 上传文件到 MinIO
     *
     * @param objectName    对象名称
     * @param multipartFile 文件
     * @throws Exception 如果上传过程中发生错误
     */
    @Override
    public void uploadFile(String objectName, MultipartFile multipartFile) throws Exception {
        String contentType = multipartFile.getContentType();
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(multipartFile.getBytes())) {
            // 计算文件哈希值
            String fileHashString = DigestUtils.sha256Hex(byteArrayInputStream);

            // 重新设置流的起始位置
            byteArrayInputStream.reset();

            // 上传文件到 MinIO
            CustomMinioClient minioClient = initializeMinioClient();
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
                    .contentType(contentType)
                    .build();
            minioClient.putObject(args);

            // 存储文件哈希值到数据库
            fileHashService.storeFileHash(objectName, fileHashString);
        } catch (MinioException e) {
            log.error("上传文件时发生错误：{}", e.toString());
            throw e;
        }
    }

    /**
     * 列出 MinIO 存储桶中的所有对象
     *
     * @return 对象名称列表
     * @throws Exception 如果列出对象过程中发生错误
     */
    @Override
    public List<String> listObjects() throws Exception {
        // 默认不递归列出所有对象
        return listObjects(false).stream().map(o -> {
            if (o.endsWith("/")) {
                return o.substring(0, o.length() - 1);
            }
            return o;
        }).toList();
    }

    /**
     * 列出 MinIO 存储桶中的所有对象
     *
     * @param recursive 是否递归列出所有对象
     * @return 对象名称列表
     * @throws Exception 如果列出对象过程中发生错误
     */
    @Override
    public List<String> listObjects(boolean recursive) throws Exception {
        return listObjects("", recursive);
    }

    /**
     * 列出 MinIO 存储桶中的所有对象
     *
     * @param prefix 对象前缀
     * @return 对象名称列表
     * @throws Exception 如果列出对象过程中发生错误
     */
    @Override
    public List<String> listObjects(String prefix) throws Exception {
        return listObjects(prefix, false);
    }

    /**
     * 列出 MinIO 存储桶中的所有对象
     *
     * @param prefix    对象前缀
     * @param recursive 是否递归列出所有对象
     * @return 对象名称列表
     * @throws Exception 如果列出对象过程中发生错误
     */
    @Override
    public List<String> listObjects(String prefix, boolean recursive) throws Exception {
        CustomMinioClient minioClient = initializeMinioClient();
        ListObjectsArgs args = ListObjectsArgs.builder()
                .bucket(bucketName)
                .prefix(prefix)
                .recursive(recursive)
                .build();
        Iterable<Result<Item>> results = minioClient.listObjects(args);
        List<String> objectNames = new ArrayList<>();
        for (Result<Item> result : results) {
            objectNames.add(result.get().objectName());
        }
        return objectNames;
    }

    /**
     * 获取 MinIO 存储桶中的对象
     *
     * @param objectName 对象名称
     * @return 对象输入流
     * @throws Exception 如果获取对象过程中发生错误
     */
    @Override
    public InputStream getObject(String objectName) throws Exception {
        CustomMinioClient minioClient = initializeMinioClient();
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build();
        return minioClient.getObject(args);
    }

    /**
     * 删除 MinIO 存储桶中的对象
     *
     * @param objectName 对象名称
     * @throws Exception 如果删除过程中发生错误
     */
    @Override
    public void removeObject(String objectName) throws Exception {
        try {
            CustomMinioClient minioClient = initializeMinioClient();
            RemoveObjectArgs args = RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build();
            minioClient.removeObject(args);

            // 清理当前对象的缓存和数据库记录
            fileHashService.deleteFileHash(objectName);
        } catch (MinioException e) {
            log.error("删除对象时发生错误：{}", e.toString());
            throw e;
        }
    }

    /**
     * 删除 MinIO 存储桶中的指定前缀的所有对象
     *
     * @param objectName 对象名称
     * @throws Exception 如果删除过程中发生错误
     */
    @Override
    public void removeObjects(String objectName) throws Exception {
        CustomMinioClient minioClient = initializeMinioClient();
        List<String> objectNames = listObjects(objectName, true);
        // 删除对象
        objectNames.forEach(o -> {
            RemoveObjectArgs args = RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(o)
                    .build();
            try {
                minioClient.removeObject(args);
            } catch (Exception e) {
                log.error("删除对象时发生错误：{}", e.toString());
                throw new RuntimeException(e);
            }
        });
        // 清理所有文件数据库哈希缓存
        fileHashService.deleteAllFileHash(objectNames);
    }

    /**
     * 检查对象是否存在于 MinIO 存储桶中
     *
     * @param objectName 对象（文件）名称
     * @return 如果对象存在则返回 true，否则返回 false
     * @throws RuntimeException 如果检查对象存在性过程中发生错误
     */
    @Override
    public boolean isObjectExist(String objectName) {
        try {
            CustomMinioClient minioClient = initializeMinioClient();
            StatObjectArgs args = StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build();
            minioClient.statObject(args);
            return true;
        } catch (ErrorResponseException e) {
            if (e.response().code() == 404) {
                return false;
            }
            throw new RuntimeException("检查对象是否存在时出错", e);
        } catch (MinioException | IOException | InvalidKeyException | NoSuchAlgorithmException e) {
            throw new RuntimeException("检查对象是否存在时出错", e);
        }
    }

    /**
     * 初始化多部分上传并返回上传的唯一 ID。
     *
     * @param objectName  对象名称。
     * @param data        文件输入流。
     * @param partSize    每个分片的大小。
     * @param contentType 文件的内容类型。
     * @return 多部分上传的唯一 ID。
     * @throws Exception 异常信息。
     */
    @Override
    public String initMultiPartUpload(String objectName, BufferedInputStream data, int partSize, String contentType) throws Exception {
        try {
            CustomMinioClient minioClient = initializeMinioClient();
            // 创建 PutObjectArgs 对象，设置桶名称和对象名称
            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .stream(data, -1, partSize)
                    .build();

            // 获取包含额外头信息和生成头信息的 Multimap 对象
            Multimap<String, String> headers = headers(args, contentType);

            // 调用自定义的 customCreateMultipartUpload 方法初始化多部分上传
            CreateMultipartUploadResponse response = minioClient.customCreateMultipartUpload(bucketName, null, objectName, headers, args.extraQueryParams());

            // 返回多部分上传的唯一 ID
            return response.result().uploadId();
        } catch (MinioException e) {
            log.error("初始化多部分上传时发生错误：{}", e.toString());
            throw new RuntimeException("初始化多部分上传时出错", e);
        }
    }

    /**
     * 上传文件的一个分片到 MinIO 服务器
     *
     * @param objectName   对象名称
     * @param data         文件输入流
     * @param contentType  文件的内容类型
     * @param objectSize   文件总大小
     * @param uploadedSize 已上传的大小
     * @param partCount    总分片数
     * @param partSize     每个分片的大小
     * @param parts        分片元数据数组
     * @param partNumber   当前分片编号
     * @param uploadId     多部分上传的 ID
     * @return 已上传的文件大小
     * @throws Exception 如果上传失败
     */
    @Override
    public long uploadFilePart(String objectName, BufferedInputStream data, String contentType, long objectSize, long uploadedSize, int partCount, int partSize, Part[] parts, int partNumber, String uploadId) throws Exception {
        // 计算当前分片的大小
        long availableSize = calculateAvailableSize(data, objectSize, uploadedSize, partCount, partSize, partNumber);

        // 创建 PutObjectArgs 对象
        PutObjectArgs args = createPutObjectArgs(objectName, data, availableSize);

        // 如果只有一个分片，直接上传
        if (partCount == 1) {
            uploadSinglePart(args, contentType, availableSize, data);
            return uploadedSize;
        }

        // 处理服务端加密头信息
        Map<String, String> ssecHeaders = extractSsecHeaders(args);

        // 分片上传
        uploadMultiPart(args, availableSize, uploadId, partNumber, ssecHeaders, parts);

        // 计算已上传大小
        uploadedSize += availableSize;
        return uploadedSize;
    }

    /**
     * 完成多部分上传
     *
     * @param objectName 对象名称
     * @param parts      分片元数据数组
     * @param uploadId   多部分上传的 ID
     * @throws Exception 如果完成上传失败
     */
    @Override
    public void completeMultipartUpload(String objectName, Part[] parts, String uploadId) throws Exception {
        try {
            CustomMinioClient minioClient = initializeMinioClient();
            minioClient.customCompleteMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
        } catch (Exception e) {
            log.error("完成多部分上传时发生错误：{}", e.toString());
            throw e;
        }
    }

    /**
     * 将指定前缀的对象集合打包成 ZIP 格式并返回输入流
     *
     * @param objectNames 要打包的对象名称列表
     * @param prefix      对象名称的前缀，用于在 ZIP 文件中去除路径
     * @return 包含 ZIP 内容的输入流
     * @throws IOException 如果发生 I/O 错误
     */
    @Override
    public InputStream getObjectAsZipStream(List<String> objectNames, String prefix) throws IOException {
        PipedOutputStream pos = new PipedOutputStream();
        final PipedInputStream pis = new PipedInputStream(pos);
        // 启动一个新线程执行 ZIP 打包操作
        new Thread(() -> {
            try (ZipOutputStream zos = new ZipOutputStream(pos)) {
                for (String objectName : objectNames) {
                    // 获取对象的输入流并写入到 ZIP 输出流中
                    try (InputStream is = getObject(objectName)) {
                        // 去除前缀部分并添加到 ZIP 条目中
                        zos.putNextEntry(new ZipEntry(objectName.substring(prefix.length())));
                        // 将对象的输入流转移到 ZIP 输出流中
                        is.transferTo(zos);
                        // 关闭当前 ZIP 条目
                        zos.closeEntry();
                    }
                }
            } catch (Exception e) {
                // 打印异常堆栈信息
                e.printStackTrace();
            }
        }).start(); // 启动线程

        // 返回管道输入流
        return pis;
    }


    /**
     * 计算当前分片的大小
     *
     * @param data         输入流
     * @param objectSize   文件总大小
     * @param uploadedSize 已上传的大小
     * @param partCount    总分片数
     * @param partSize     每个分片的大小
     * @param partNumber   当前分片编号
     * @return 可用的分片大小
     * @throws Exception 如果计算失败
     */
    private long calculateAvailableSize(BufferedInputStream data, long objectSize, long uploadedSize, int partCount, int partSize, int partNumber) throws Exception {
        long availableSize = partSize;
        if (partCount > 0) {
            if (partNumber == partCount) {
                availableSize = objectSize - uploadedSize;
            }
        } else {
            CustomMinioClient minioClient = initializeMinioClient();
            availableSize = minioClient.getAvailableSize(data, partSize + 1L);
            if (availableSize <= partSize) {
                partCount = partNumber;
            } else {
                availableSize = partSize;
            }
        }
        return availableSize;
    }

    /**
     * 创建 PutObjectArgs 对象
     *
     * @param objectName    对象名称
     * @param data          输入流
     * @param availableSize 可用的分片大小
     * @return PutObjectArgs 对象
     */
    private PutObjectArgs createPutObjectArgs(String objectName, BufferedInputStream data, long availableSize) {
        return PutObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .stream(data, availableSize, -1)
                .region(null)
                .build();
    }

    /**
     * 上传单个分片到 MinIO 服务器
     *
     * @param args          PutObjectArgs 对象
     * @param contentType   文件的内容类型
     * @param availableSize 可用的分片大小
     * @param data          输入流
     * @throws Exception 如果上传失败
     */
    private void uploadSinglePart(PutObjectArgs args, String contentType, long availableSize, BufferedInputStream data) throws Exception {
        Multimap<String, String> headers = headers(args, contentType);
        CustomMinioClient minioClient = initializeMinioClient();
        minioClient.customPutObject(bucketName, null, args.object(), data, (int) availableSize, headers, args.extraQueryParams());
    }

    /**
     * 创建并返回一个包含额外头信息和生成头信息的 Multimap 对象。
     * 如果没有设置 Content-Type 头信息，则添加 Content-Type 头信息。
     *
     * @param args        PutObjectArgs 对象，包含上传请求的参数。
     * @param contentType 文件的内容类型。
     * @return 包含额外头信息和生成头信息的 Multimap 对象。
     */
    private Multimap<String, String> headers(PutObjectArgs args, String contentType) {
        // 创建一个新的 Multimap 对象，用来存储头信息
        Multimap<String, String> headers = newMultimap(args.extraHeaders());

        // 将 PutObjectArgs 对象中的生成头信息添加到 Multimap
        headers.putAll(args.genHeaders());

        // 如果头信息中不包含 Content-Type，则添加 Content-Type 头信息
        if (!headers.containsKey("Content-Type")) {
            headers.put("Content-Type", contentType);
        }

        // 返回包含额外头信息和生成头信息的 Multimap 对象
        return headers;
    }

    /**
     * 提取服务端加密头信息
     *
     * @param args PutObjectArgs 对象
     * @return 加密头信息的 map
     */
    private Map<String, String> extractSsecHeaders(PutObjectArgs args) {
        if (args.sse() != null && args.sse() instanceof ServerSideEncryptionCustomerKey) {
            return args.sse().headers();
        }
        return null;
    }

    /**
     * 上传多个分片到 MinIO 服务器
     *
     * @param args          PutObjectArgs 对象
     * @param availableSize 可用的分片大小
     * @param uploadId      多部分上传的 ID
     * @param partNumber    当前分片编号
     * @param ssecHeaders   加密头信息
     * @param parts         分片元数据数组
     * @throws Exception 如果上传失败
     */
    private void uploadMultiPart(PutObjectArgs args, long availableSize, String uploadId, int partNumber, Map<String, String> ssecHeaders, Part[] parts) throws Exception {
        CustomMinioClient minioClient = initializeMinioClient();
        UploadPartResponse response = minioClient.customUploadPart(args.bucket(), args.region(), args.object(), args.stream(), (int) availableSize, uploadId, partNumber, ssecHeaders != null ? Multimaps.forMap(ssecHeaders) : null, null);
        String etag = response.etag();
        parts[partNumber - 1] = new Part(partNumber, etag);
    }

    /**
     * 创建一个新的 Multimap 实例
     *
     * @param map 现有的 Multimap
     * @return 新的 Multimap 实例
     */
    private Multimap<String, String> newMultimap(Multimap<String, String> map) {
        return map != null ? HashMultimap.create(map) : HashMultimap.create();
    }
}
